/**
 * Copyright (C) 2010 altuure <altuure [AT] gmail [DOT] com> http://www.altuure.com/projects/yagdao
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.altuure.yagdao.hibernate;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.NonUniqueResultException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.impl.CriteriaImpl;

import com.altuure.yagdao.SearchResultList;
import com.altuure.yagdao.YFetch;
import com.altuure.yagdao.YLimit;
import com.altuure.yagdao.YMethod;
import com.altuure.yagdao.YOperator;
import com.altuure.yagdao.YPage;
import com.altuure.yagdao.YParameter;
import com.altuure.yagdao.common.GenericDAOFactory;
import com.altuure.yagdao.common.Utils;

/**
 * hibernate dao factory.
 * @author altuure
 */
public class GenericHibernateDAOFactory extends GenericDAOFactory {
    private static final Log log = LogFactory.getLog(GenericHibernateDAOFactory.class);

    /**
     * created proxy instance.
     */
    @SuppressWarnings("rawtypes")
    private GenericHibernateDAO genericHibernateDAO;
    /**
     * injected utility to access hibernate session.
     */
    private HibernateSessionAccessor sessionAccessor;

    /**
     * default cons.
     */
    public GenericHibernateDAOFactory() {
        setTargetDAOImpl(GenericHibernateDAO.class);
    }

    /**
     * created dao proxy class.
     * @return dao proxy class
     */
    public Class getObjectType() {
        if (daoClass == null)
            try {
                initClass();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        return genericHibernateDAO.getClass();
    }

    /**
     * return instance.
     * @return
     * @throws Exception
     */
    public Object getObject() throws Exception {
        return genericHibernateDAO;
    }

    /**
     * is singleton.
     * @return true;
     */
    public boolean isSingleton() {
        return true; // todo : implement here
    }

    public void setTargetInterfaces(Class targetInterfaces) {
        this.targetInterfaces = targetInterfaces;
    }

    /**
     * create proxy class.
     */
    public void initClass() {
        InvocationHandler h = initHandler();
        if (h == null)
            return;
        genericHibernateDAO = (GenericHibernateDAO) Utils.createInstance(h, targetDAOImpl, targetInterfaces);
        this.daoClass = genericHibernateDAO.getClass();
        initObjectClass();

    }

    /**
     * init DAO
     * @throws Exception init exception
     */
    protected void initDAO() throws Exception {

        initClass();

        genericHibernateDAO.setSessionAccessor(sessionAccessor);
        genericHibernateDAO.setObjectClass(objectClass);

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.altuure.yagdao.common.GenericDAOFactory#saveMethod(java.lang.Object,
     * java.lang.reflect.Method, java.lang.Object[])
     */
    protected Object saveMethod(Object obj, Method method, Object[] args) throws Throwable {
        Object o = objectClass.newInstance();
        setValues(o, method, args);
        getSession().save(o);
        return o;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.altuure.yagdao.common.GenericDAOFactory#updateMethod(java.lang.Object
     * , java.lang.reflect.Method, java.lang.Object[])
     */
    protected Object updateMethod(Object obj, Method method, Object[] args) throws Throwable {
        log.info("GenericHibernateDAOFactory.intercept");

        Object o = genericHibernateDAO.load((Serializable) args[0]);
        setValues(o, method, args);
        getSession().update(o);
        getSession().flush();
        return o;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.altuure.yagdao.common.GenericDAOFactory#criteriaMethod(java.lang.
     * Object, java.lang.reflect.Method, java.lang.Object[])
     */
    protected Object criteriaMethod(Object obj, Method method, Object[] args) throws Throwable {
        log.debug("GenericHibernateDAOFactory.criteriaMethod");
        final CriteriaImpl rootCriteria = (CriteriaImpl) genericHibernateDAO.createCriteria();

        Map<String, String> aliasMap = new HashMap<String, String>();
        YPage paging = findDAOPage(method, args);

        setProjections(rootCriteria, method, aliasMap);
        createSearchCriteria(method, args, rootCriteria);

        YFetch daoFetch = method.getAnnotation(YFetch.class);
        if (daoFetch == null)
            daoFetch = (YFetch) objectClass.getAnnotation(YFetch.class);
        genericHibernateDAO.setFetchModes(rootCriteria, daoFetch);
        if (objectClass.equals(method.getReturnType()))
            return rootCriteria.uniqueResult();
        // if(org.apache.commons.lang.ClassUtils.isAssignable(method.getReturnType(),Collection.class))

        genericHibernateDAO.setOrders(rootCriteria, paging);
        genericHibernateDAO.setLimits(rootCriteria, paging);
        if (SearchResultList.class.isAssignableFrom(method.getReturnType())) {
            return new SearchResultList((List) rootCriteria.list(), (Long) countMethod(obj, method, args), paging);
        } else {
            return rootCriteria.list();
        }
    }

    protected Object queryMethod(Object obj, Method method, Object[] args) throws Throwable {
        log.debug("GenericHibernateDAOFactory.search");

        YMethod daoMethod = method.getAnnotation(YMethod.class);
        YLimit paging = findDAOPage(method, args);

        Query query = createQuery(daoMethod);
        setQueryParameters(method, args, query);
        @SuppressWarnings("rawtypes")
        SearchResultList resultList = genericHibernateDAO.query(query, paging);
        if (objectClass.equals(method.getReturnType())) {
            if (resultList.isEmpty())
                return null;
            if (resultList.size() == 1)
                return resultList.get(0);
            else
                throw new NonUniqueResultException(resultList.size());
        }

        return resultList;

    }

    /**
     * Set query parameters from query parameter annotaions
     * @param method Query method
     * @param args arguments
     * @param query query
     */
    private void setQueryParameters(Method method, Object[] args, Query query) {
        final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            final Annotation[] annotations = parameterAnnotations[i];
            final Object arg = args[i];
            YParameter daoMethodParameter = null;
            for (final Annotation annotation1 : annotations) {
                if (annotation1 instanceof YParameter) {
                    daoMethodParameter = (YParameter) annotation1;
                    break;
                }
            }
            if (daoMethodParameter != null) {
                final String name = daoMethodParameter.value();
                query.setParameter(name, arg);

            } else if (!(arg instanceof YLimit)) {
                query.setParameter(i, arg);
            }
        }
    }

    /**
     * Create Query from method annotations
     * @param daoMethod method annotation
     */
    private Query createQuery(YMethod daoMethod) {
        Query query = null;
        if (!Utils.isEmpty(daoMethod.query()))
            query = getSession().createQuery(daoMethod.query());
        else if (!Utils.isEmpty(daoMethod.queryName())) {
            query = getSession().getNamedQuery(daoMethod.queryName());
        } else {
            throw new IllegalArgumentException("queryName or query must be defined");
        }
        return query;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.altuure.yagdao.common.GenericDAOFactory#countMethod(java.lang.Object,
     * java.lang.reflect.Method, java.lang.Object[])
     */
    protected Object countMethod(Object obj, Method method, Object[] args) throws Throwable {
        log.debug("countMethod");
        final CriteriaImpl rootCriteria = (CriteriaImpl) genericHibernateDAO.createCriteria();
        createSearchCriteria(method, args, rootCriteria);

        return genericHibernateDAO.count(rootCriteria);

    }

    /**
     * returns current session from sessionAccessor.
     * @return current session.
     */
    private Session getSession() {
        return sessionAccessor.getCurrentSession();
    }

    private void createSearchCriteria(Method method, Object[] args, CriteriaImpl rootCriteria) {
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();

        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] parameterAnnotation = parameterAnnotations[i];
            if (parameterAnnotation == null)
                continue;
            if (args[i] == null)
                continue;
            if (args[i] instanceof String) {
                String s = (String) args[i];
                if (Utils.isEmpty((String) s))
                    continue;
            }

            for (Annotation annotation1 : parameterAnnotation) {
                if (annotation1 instanceof YParameter) {
                    YParameter daoMethodParameter = (YParameter) annotation1;
                    YOperator operator = daoMethodParameter.operator();
                    String s = daoMethodParameter.value();
                    String property = s;
                    Criterion criterion = null;
                    String[] strings = Utils.splitDot(s);
                    Criteria criteria = rootCriteria;

                    if (strings.length != 1) {
                        property = strings[strings.length - 1];
                        criteria = genericHibernateDAO.findCriteria(rootCriteria, strings);
                    }
                    switch (operator) {
                    case NONE:
                        break;
                    case EQUAL:
                        criterion = Restrictions.eq(property, args[i]);
                        break;
                    case GE:
                        criterion = Restrictions.ge(property, args[i]);

                        break;
                    case GT:
                        criterion = Restrictions.gt(property, args[i]);
                        break;
                    case IN:

                        Object[] objects = (Object[]) args[i];
                        if (objects.length > 0)
                            criterion = Restrictions.in(property, objects);
                        break;
                    case LE:
                        criterion = Restrictions.le(property, args[i]);
                        break;
                    case LIKE:
                        if (Utils.isNotEmpty((String) args[i]))
                            criterion = Restrictions.like(property, (String) args[i], MatchMode.ANYWHERE);
                        break;
                    case ULIKE:
                        if (Utils.isNotEmpty((String) args[i]))
                            criterion = Restrictions.ilike(property, (String) args[i], MatchMode.ANYWHERE);
                        break;
                    case LT:
                        criterion = Restrictions.lt(property, args[i]);
                        break;
                    case BETWEEN:
                        if (args[i] instanceof Object[]) {
                            Object objs[] = (Object[]) args[i];
                            if (objs[0] != null && objs[1] != null)
                                criterion = Restrictions.and(Restrictions.ge(property, objs[0]),
                                        Restrictions.le(s, objs[1]));
                            else if (objs[0] != null)
                                criterion = Restrictions.ge(property, objs[0]);
                            else if (objs[1] != null)
                                criterion = Restrictions.le(property, objs[1]);
                        } else if (args[i] instanceof long[]) {
                            long objs[] = (long[]) args[i];
                            if (objs[0] > 0)
                                criterion = Restrictions.ge(property, objs[0]);
                            if (objs[1] > 0)
                                criterion = Restrictions.le(property, objs[1]);
                        } else if (args[i] instanceof int[]) {
                            int objs[] = (int[]) args[i];
                            if (objs[0] > 0)
                                criterion = Restrictions.ge(property, objs[0]);
                            if (objs[1] > 0)
                                criterion = Restrictions.le(property, objs[1]);
                        }

                        break;
                    }
                    if (criterion == null)
                        continue;
                    if (daoMethodParameter.not())
                        criterion = Restrictions.not(criterion);
                    criteria.add(criterion);

                }
            }
        }
    }

    /**
     * set Projecttions if necessary
     * @param rootCriteria root
     * @param method method
     */
    private void setProjections(CriteriaImpl rootCriteria, Method method, Map<String, String> aliasMap) {
        YMethod annotation = method.getAnnotation(YMethod.class);
        if (annotation == null || annotation.select() == null || annotation.select().length == 0)
            return;

        String[] selects = annotation.select();
        ProjectionList projections = Projections.projectionList();

        for (int j = 0; j < selects.length; j++) {
            String property = selects[j];
            int split = property.lastIndexOf(".");

            if (split > 0) {

                String parent = property.substring(0, split);
                String child = property.substring(split + 1);
                String alias = null;
                if (aliasMap.containsKey(parent))
                    alias = aliasMap.get(parent);
                else {
                    rootCriteria.createAlias(parent, alias = "obj_" + j);
                    aliasMap.put(parent, alias);
                }
                projections.add(Projections.property(alias + "." + child));
            } else {
                projections.add(Projections.property(property));
            }

        }

        rootCriteria.setProjection(projections);

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.altuure.yagdao.common.GenericDAOFactory#deleteMethod(java.lang.Object
     * , java.lang.reflect.Method, java.lang.Object[])
     */
    protected Object deleteMethod(Object obj, Method method, Object[] args) throws Throwable {
        log.info("GenericHibernateDAOFactory.intercept");
        Object o = getSession().get(objectClass, (Serializable) args[0]);
        getSession().delete(o);
        return o;
    }

    /**
     * set object class by default.
     * @param objectClass
     */
    public void setObjectClass(Class objectClass) {
        this.objectClass = objectClass;
    }

    /**
     * get utility.
     * @return {@link #sessionAccessor}
     */
    public HibernateSessionAccessor getSessionAccessor() {
        return sessionAccessor;
    }

    /**
     * current session access util.
     * @param sessionAccessor access util
     */
    public void setSessionAccessor(HibernateSessionAccessor sessionAccessor) {
        this.sessionAccessor = sessionAccessor;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    public String toString() {
        return "GenericHibernateDAOFactory<" + objectClass + ">";
    }

    /**
     * create new Instance of given interface or abstract class
     * @param clz interface
     * @param acc session accessor
     * @return instance of given interface
     * @throws Exception creation exception
     */
    @SuppressWarnings("rawtypes")
    public static Object createInstance(Class clz, HibernateSessionAccessor acc) throws Exception {
        GenericHibernateDAOFactory daoFactory = new GenericHibernateDAOFactory();
        daoFactory.setTargetInterfaces(clz);
        daoFactory.setSessionAccessor(acc);
        daoFactory.initDAO();
        return daoFactory.getObject();
    }

}
